4

链接

——写在前面

1、为什么要自己手写一个脚手架?

在写这个脚手架之前我也深深的问过自己,在我工作的项目中需要去重新写一个脚手架吗?或者说有那么多已经写好的脚手架为何不采用?

事情的经过是这样的,在很早很早以前,我尝试使用过VUE-CLI 2进行过项目开发,当时并不怎么熟悉webpack以及一些打包编译的相关知识,随着页面的增加!项目的体积的增大,导致整体build出来的包非常之大,公共文件也会随之增大,加载速度也会随之降低。后续的结果我就不做阐述了!

那么!后来... vue-Cli 3.0诞生了,首次使用简直是个救世的主,无论从速度还是编译过程体验都非常好,而且还可以通过vue.config.js自定义很多的配置,基本上完全可以自定义了,当然!也是随着页面的不断增加核项目的增大,在这个时候。我开始发现我自己对于webpack或者说打包编译的相关知识已经不能支撑我继续自定义的开发下去了。发现了一些潜在的问题,但是并没有实际的解决思路的时候,就可以追述到一些基础知识的欠缺。

随着项目的逐渐增大,尤其是多页应用的支持以及一些文件模块化的拆分,包括一些tree-shaking的运用。尽管vue-cli3.0支持configureWebpack 这样强大的API。但是仔细想想,要想从事情的本质或根本上解决问题,首先自身要相对的熟悉,并在此基础之上运用和操作,得以充分的发挥;所以还是决定自己去了解以便更好的开发。

2、如何去思考遇到的问题?

在项目的开发中,尤其是在写脚手架这种工具性的东西的时候,需要考虑到的场景和实际运用的时候,更多的是不能沉浸在自己的思维之中,参考并学习别人的经验是有必要的,从而得出一套符合自己的思路。

从最开始的目录结构,以及模块化的一些思考,如何更好的做到性能的优化等等,都是值得思考的问题所在,如何处理好自己的业务逻辑,针对不同的项目以及兼容性的考虑等等。

——正文

在此之前我们需要对webpack4的一些文档或者API进行充分的了解,可以参考官方文档或者参考印记中文的webpack文档,但是针对于webpack4的文档本来介绍的不是很全面,在很多的API上面还是之前的介绍,所以,有很多小伙伴在看文档的时候发现并不能正常的进行操作,这时候可以结合两个不同版本的文档进行研究,当然时间的消耗成本也是比较高的。

3、一些基本的构建思路!
在此之前我将控制业务逻辑的代码进行分离,脚手架是单独存在的。两者目录结构相互独立,业务逻辑的代码永远不会干涉到脚手架的

对于一些最基础的配置我就不一一讲述了:

webpack.config.js

module.exports = {
    mode:'development',
    entry: './***.js',
    output: {
        path: path.resolve(__dirname, 'dist'),
        filename: '[name].js'
    },
    module:{
        //...
    },
    plugins:[
        //...
    ]
};

以上是一些基本的配置方式,当然我们可以通过package.json文件中的scripts选项自定义一些基于命令行的配置:

package.josn

 "scripts": {
    "dev": "APP_ENV=dev webpack-dev-server --config core/dev.js",
    "prd": "APP_ENV=build webpack --config core/build.js",
    "build": "APP_ENV=build webpack --config core/build.js",
    "lint": "eslint --fix */*.vue *.js",
    "test:whale": "jest tests/*.js && npm run build"
  },

以上的一些配置仅供参考,分别可以通过npm run dev | npm run prd | ...来进行相关的操作。关于更多的npm script 详细参考

4、node_modules的一些介绍

关于node_modules这块其实非常的庞大,这也是npm的一个巨大的社区。与其说是学习,不如说是抽一部分优秀的包来使用,要知道一个node_modules包是在做什么事情,可以通过npm搜索包进行浏览其详细介绍,在对应的github地址进行相应的了解。

关于如何学习node_modules包并不在我的介绍范围内,但是我会介绍一些常用的一些比较好用的包进行分享。而且在构建脚手架之前,必须要对很多的包进行相关的学习,不需要知道每个包的源码是什么,但是至少需要知道一些包的作用和用法,比如.vue文件需要使用vue-loader进行解析,使用到一些语法校验的时候需要用到eslint,基于webpack进行生成html页面的时候需要用到html-webpack-plugin;ES6ES5需要用到一系列的@babel/xxx插件,等等..

我这里不做一一的介绍,但是会在后续用到每一个包的时候做相对的介绍即可。

——多入口的输入和编译的输出

多入口的输入相对比较简单,可以直接参考官方文档
当然entry可以接受一个对象进行多页面的输入,如果只是起步阶段建议使用一个入口文件进行编写,例:

module.exports = {
    entry: index:'xxx/xxx/index.js'
}
////或
module.exports = {
    mode: isProd ? 'production' : 'development',
    entry: {
        index:'xxx/xxx/index.js'
    }
}

输出可以根据自己的需要配置output参数:

let path = require('path');
output: {
    filename: './js/[name].js',
    chunkFilename: './js/[name].js',
    path: path.resolve(__dirname, '../build/')
},
关于module | plugins | optimization | ...等模块的配置我就不详细的说了,但是主要还是要说一下各个模块之间的配合使用。
5、module模块的优化

首先module.noParse是一个必备的参数,可以忽略一下大型的已经构建过的模块,从而提高构建的性能,这里放一个案例:

webpack.config.js

module:{
    noParse:/^(vue|vue-router|vuex|vuex-router-sync|lodash|echarts|axios)$/,
}

以上的案例忽略了vue | vue-router | vuex | ...等大型额已经构建过的模块。(通俗易懂的说一下,webpack在打包时会将所有用到的模块进行打包编译,在这里只是忽略了重新构建的过程,但是对于chunk的时候依然还是会在内存堆里面使用并打包。)

6、比较复杂的module.rules

其实相对来说,在webapck 4module.rules还是和之前一样的使用,针对不同的后缀的文件进行不同的处理,在这里主要说一下借助了happypack进行多线程处理,当然你也可以从npm网站详细了解。与此同时我也选择放弃使用DllPlugin | DLLReferencePlugin插件,下面我就说一下,如何选择?为何放弃?

我们都知道webpack在打包编译时是单线程的运作,但是我们的电脑已经非常强大,单线程的处理,第一是随着项目的增大时间会增长,第二是有些空闲的cpu等硬件设备得不到充分的利用,这是有我们选择开启多线程的操作,在node中是可以开启多线程的,只是我们借助了happypack这样的工具来进行多线程的控制和使用,在webpack中,我们可能需要处理到.js文件,也可能会用到.css文件,那么在打包的时候会将所有的字符汇聚到内存中,进行大量的密集型运算,并提取拆分成不同的块,这时候我们借助多线程来处理即可,使用方法如下:

首先在当前项目中执行命令行npm i happypack -D在开发环境中安装,引入:

const webpack = require('webpack');

例如我们再处理.js文件时候需要使用cache-loaderbabel-loader时,只需要配置:

{
    test: /\.js$/,
    use: ['happypack/loader?id=babel' ],
    exclude: /node_modules/
}

并在plugins选项中配置:

new HappyPack({
    id: 'babel',
    cache: true,
    threads: require('os').cpus().length, //开几个线程去处理 
    loaders: [ 'cache-loader','babel-loader?cacheDirectory' ]
    verbose: true,         //允许 HappyPack 输出日志 ,默认true
    //threadPool: happyThreadPool,
})

需要注意的只是rules中调用的HappyPackidplugins中实例化的id相同即可。

最后说一下为何要放弃DllPlugin,这个插件是生成一个动态的链接文件,也就是你把你认为不需要多次重复编译的文件通过DllPlugin插件如生成一个.js.json文件,当你第二次进行打包编译的时候再通过DllReferencePlugin进行引入使用,这样就会大大减少了编译的数量,好处是可以多一些固定的模块包进行减少处理,但是后来我发现,在项目中只有模块拆分的足够细致时候这个确实有不少作用,否则徒增一些步骤,因为每次在拆分块的时候,很多的模块是会进行重新组装的。(以上只是个人观点,仅供参考~)

7、介绍几个简单的plugins用法

如果是在开发环境,我们需要对页面进行热更新,我们可以开启:

plugins:[
    new webpack.NamedModulesPlugin(),
    new webpack.HotModuleReplacementPlugin()
]

NamedModulesPlugin是现实热更新的模块的名称,HotModuleReplacementPlugin启用HRM官方有介绍
如果你是使用VUE开发项目,那么new VueLoaderPlugin()是必不可少的了。
注入一些全局变量使用参考:

new webpack.DefinePlugin({
    //...
}),

重点的说一下html-webpack-plugin这个插件,用于生成过一些页面的小伙伴应该都有所了解,那么在生成多个页面的时候,我们就需要new HtmlWebpackPlugin({ ... })多个实例即可,通常情况下,我们会通过循环入口文件进行循环注入一个数组中即可。
另外备注一部分参数的说明:

new HtmlWebpackPlugin({
    title: PAGES[k].title || 'title',
    chunks: chunks,
    filename:`${k}.html`,
    minify: {
        removeComments: true,       //Strip HTML comments
        collapseWhitespace: true,   //折叠有助于文档树中文本节点的空白区域
    },                    //对html进行压缩,默认false
    hash: PAGES[k].hash === true ? true : false,      //默认false
    template: PAGES[k].template,
    excludeChunks: excludeChunks,
    favicon:PAGES[k].favicon || ''
    // chunksSortMode:"dependency"
    /**
     * 'dependency' 按照不同文件的依赖关系来排序。
     * 'auto' 默认值,插件的内置的排序方式,具体顺序我也不太清楚...
     * 'none' 无序? 不太清楚...
     * 'function' 提供一个函数!!复杂...
     */
})

上面的代码只是截取的部分代码片段,有一些值是需要做一些相应的处理,关于HtmlWebpackPlugin插件的详细参考

这里需要注意的是,当我们对项目包中的公共代码做了不同的splitChunks(下面会讲解这个模块)时候,比如像chunks默认会全部注入进入页面,所以我么你可能需要手动进行一些处理,或者使用excludeChunks对一些块进行排除,其排除的是你最终生成的代码文件名称。template是指对应的模版。更加详细的参考github 文档等。
8、敲黑板、讲重点的optimization.splitChunks

当然目前这部分的文档在官网还不是很全,所以这里我们参考了印记中文webpack的说明文档,optimization指优化模块。splitChunks可以翻译为拆分块,默认的配置参数参考官方文档即可,重要的说一下optimization.splitChunks.cacheGroups,非常强大的一个API,先说什么时候会用到这个功能,这就对应了我们最前面所说的,vue-cli3脚手架不太方便的地方,当项目包逐渐增大的同时,通常情况下,会为我们提供一个公共文件,在vue-cli3脚手架中,为我们提取了 vendor-chunks.js为所有文件的公共文件,但是如果我们有一个场景,其中的某一个页面根本不需要依赖一些包的,且这个包相对较大的同时,我们另可多发一次请求,也不需要去加载这些多余的文件,我们就可以通过optimization.splitChunks.cacheGroups将这部分公共的提取出来,在对制定的页面在HtmlWebpackPlugin插件中将它排除即可(或者采用注入指定的chunk的形式),举一个例子,我们可以吧所有页面中的vue相关的源码包提取到一个单独的文件,我们可以采用如下的配置:

optimization: {
    splitChunks: {
        minSize: 30000,
        //缓存组
        cacheGroups: {
            vue: {
                test: /([\/]node_modules[\/]vue)/,  // <- window | mac -> /node_modules/vue/
                name: 'vue-vendor',                 //拆分块的名称
                chunks: 'initial',                  //initial(初始块)、async(按需加载块)、all(全部块),默认为all;
                priority: 100,                      // 该配置项是设置处理的优先级,数值越大越优先处理
                enforce: true,                      // 如果cacheGroup中没有设置minSize,则据此判断是否使用上层的minSize,true:则使用0,false:使用上层minSize
                //minSize: 1024*10,                 //表示在压缩前的最小模块大小,默认为0;
                //minChunks: 1,                     //表示被引用次数,默认为1;
                //maxAsyncRequests:                 //最大的按需(异步)加载次数,默认为1;
                //maxInitialRequests:               //最大的初始化加载次数,默认为1;
                //reuseExistingChunk: true          //表示可以使用已经存在的块,即如果满足条件的块已经存在就使用已有的,不再创建一个新的块。
            }
        }
    },

}

当然你可以根据自己的需要进行多个的配置,名称可以自定义,test是过滤的方式,这里核心要说明的是priority(优先权),当然是数字越大优先权越高,什么意思,当我们在进行webpack打包的同时,会将我们所有用到的代码全部加载在内存中,进行进行转换和编译等操作,拆分块的核心在于,将一些公共的模块拆分成多个模块,按照优先级进行提取出去,生成一个文件,然后再去查找下一个优先级的进行提取。这里如何进行包拆分可以借助webpack-bundle-analyzer插件进行可视化的进行操作。其用法是直接npm i webpack-bundle-analyzer

const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;

webpack中配置plugin选项新增即可:

new BundleAnalyzerPlugin({
    defaultSizes:'gzip',
    logLevel:'warn'
}),

在执行项目的时候会进行包依赖的详细分析,此时我们可以通过可视化的方式进行更好的拆分。

splitChunks也是在webpack4鼎力打造的一个功能,在于理解其中的用法,拆分的代码块更多的时候是要我们在html-webpack-plugin的时候更好的生成每一个独立的页面,有一些框架中采用了每增加一个文件就回去自动添加一个新的页面的方式,当然这样的额缺点是必须按照对应的目录进行生成对应的页面,这里我们采用了手动配置的方式生成新的页面,每生成一个页面,会默认的注入一些拆分出来的代码块。
虽然html-webpack-plugin没生成一个新的html页面必须要实例化一个新的对象。所以我们可以通过配置文件的循环进行生成对应的配置:

new HtmlWebpackPlugin({
            title: PAGES[k].title || 'title',
            chunks: chunks,
            filename:`${k}.html`,
            minify: {
                removeComments: true,       //Strip HTML comments
                collapseWhitespace: true,   //折叠有助于文档树中文本节点的空白区域
            },    //对html进行压缩,默认false
            hash: PAGES[k].hash === true ? true : false,      //默认false
            template: PAGES[k].template,
            excludeChunks: excludeChunks,
            favicon:PAGES[k].favicon || ''
            // chunksSortMode:"dependency"
            /**
             * 'dependency' 按照不同文件的依赖关系来排序。
             * 'auto' 默认值,插件的内置的排序方式,具体顺序我也不太清楚...
             * 'none' 无序? 不太清楚...
             * 'function' 提供一个函数!!复杂...
             */
        })

上面是一些脚手架中的源代码,在一些简单的页面,可以通过排除的方式省去一些文件的加载,而不是通用的加载一个较大的文件包。在优化的过程中我们可以减少http请求,但是我们也可以减少请求包的大小。

放弃使用DllPlugin | DLLReferencePlugin

经过一段时间的考量,我发现关于DllPlugin & DLLReferencePlugin这两个插件的使用只有在一部分情况下才比较适合,当你的项目包中用一个不需要重新构建的模块的时候你再使用这个插件是比较合适的,然而很多时候,我们每次的构建几乎都会重新编译我们的代码,当然他们的使用方式是先通过DllPlugin去打包好不不需要重新构建的文件,同时生成manifest.json文件,在下次编译的同时通过DLLReferencePlugin进行载入即可,减少了一些包的重复编译。

总结

写在最后,这一块所包含的信息量相对较多,同时需要对项目构建有一定程度的了解,在很多的过程中是需要去思考一个问题的解决方法和方式,而不仅仅是追求使用,每一个工具都会提供强大的API和功能以适合众多的业务场景。对于一些通过不同的方式得到同样结果的问题就仁者见仁吧!


言图iantoo
723 声望19 粉丝

行到水穷处,坐看云起时